package com.xinqi.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xinqi.dto.Result;
import com.xinqi.entity.Blog;
import com.xinqi.entity.LikedIdTime;
import com.xinqi.entity.User;
import com.xinqi.mapper.BlogMapper;
import com.xinqi.service.BlogService;
import com.xinqi.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Service;
import redis.clients.jedis.*;

import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements BlogService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Override
    public List<Blog> getPageByLimited(Page<Blog> pageParam) {
        Page<Blog> blogPage = baseMapper.selectPage(pageParam, null);
        List<Blog> records = blogPage.getRecords();
        return records;
    }

    @Override
    public Integer queryBlogLikesNumber(Long id) {
        String key = "BLOG_LIKED_KEY" + id;
        Set<String> set = stringRedisTemplate.opsForZSet().range(key, 0, -1);
        Integer likeNumber = 0;
        if (set != null) likeNumber = set.size();
        return likeNumber;
    }//这里的数据持久化就由定时任务去执行吧

    @Override
    public Result likeBlog(Long blogId, Long userId) {

        String key = "BLOG_LIKED_KEY" + blogId;
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        if (score == null) {
            //如果未点赞，可以点赞
            // 保存用户到Redis的set集合
            stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
        } else {
            //如果已点赞，取消点赞
            //把用户从Redis的set集合移除
            stringRedisTemplate.opsForZSet().remove(key, userId.toString());
        }
        return Result.ok();
    }

    //在redis里查询该博客下所有点赞的用户ids
    @Override
    public List<Long> queryAllBlogLikesUserIds(Long id) {
        String key = "BLOG_LIKED_KEY" + id;
        return queryAllBlogLikesUserIdsByRedisKey(key);
    }

    private List<Long> queryAllBlogLikesUserIdsByRedisKey(String key) {
        Set<String> stringUserList = stringRedisTemplate.opsForZSet().range(key, 0, -1);
        if (stringUserList == null || stringUserList.isEmpty()) return Collections.emptyList();

        List<Long> ids = stringUserList.stream().map(Long::valueOf).collect(Collectors.toList());

        return ids;
    }

    @Autowired
    private UserService userService;

    //在redis里查询该篇博客里所有点赞的用户users
    @Override
    public List<User> getAllBlogLikesUsersByBlogId(Long id) {
        List<Long> ids = queryAllBlogLikesUserIds(id);

        String idStr = StrUtil.join(",", ids);
        // 根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
        List<User> users = new ArrayList<>(userService.query()
                .in("id", ids).last("ORDER BY FIELD(id," + idStr + ")")
                .list());
        return users;
    }

    //查询redis里的likeList，以Map<String,String> 返回
    @Override
    public Map<String, String> queryAllBlogLikesUserIdWithScores(Long id) {
        String key = "BLOG_LIKED_KEY" + id;
        return queryAllBlogLikesUserIdWithScoresByRedisKey(key);
    }

    //通过BLOG_LIKED_KEY1拿到{1=time1，2=time2}，是map对象，这个map对象的k-v对应zset的value和score
    private Map<String, String> queryAllBlogLikesUserIdWithScoresByRedisKey(String key) {
        Set<String> stringUserIdList = stringRedisTemplate.opsForZSet().range(key, 0, -1);

        if (stringUserIdList == null || stringUserIdList.size() == 0) return new HashMap<>();

        List<String> ids = stringUserIdList.stream().map(String::valueOf).collect(Collectors.toList());
        HashMap<String, String> userIdWithTime = new HashMap<>();
        for (String userId : stringUserIdList) {//暂时找不到可以直接返回List<Double>的api
            String score = stringRedisTemplate.opsForZSet().score(key, userId).toString();
            userIdWithTime.put(userId, score);
        }
        return userIdWithTime;
    }

    //持久化redis里的某篇博客的likeList到数据库
    @Override
    public void durableForLikeListByBogId(Long id) {
        //通过博客id得到该篇博客文章下的所有likeList
        Map<String, String> userIdWithTime = queryAllBlogLikesUserIdWithScores(id);
        //什么都没有那肯定不操作
        if (userIdWithTime == null || userIdWithTime.size() == 0) return;

        updateLikeListByBlogId(id,userIdWithTime);

    }

    /**
     * 通过blog_like的id字段更新like_list，like_list从userIdWithTime获得。
     * @param blogId
     * @param userIdWithTime
     * @return
     */
    private boolean updateLikeListByBlogId(Long blogId,Map<String, String> userIdWithTime){
        String stringIds = userIdWithTime.toString();

        String updateSql = "like_list = " + "'" + stringIds + "' ," +
                "like_number = " + userIdWithTime.size();
        boolean isSuccess = update()
                .setSql(updateSql)
                .eq("id", blogId)
                .update();
        return isSuccess;
    }
    //将数据库里的likeList拿出来
    @Override//加载数据
    public HashMap<String, String> getIdsInTableByBlogId(Long id) {
        Blog blog = getById(id);
        String likeList = blog.getLikeList();
        HashMap<String, String> hashMap = (HashMap<String, String>) JSONUtil.toBean(likeList, Map.class);
        return hashMap;
    }


    //这里就执行所有博客的likeList入库吧
    @Override
    public void updateAllLikeListToDatabase() {
        String prefix = "BLOG_LIKED_KEY";
//        Set<String> keys = getAllRedisKeyByKeys(prefix);//这个不行！因为底层是使用了keys xxx*,可能会导致阻塞，有风险，除非从节点则也许可以
        Set<String> keys = getAllRedisKeyByScanThroughMatch(prefix);//通过scan拿到以BLOG_LIKED_KEY开头的所有key

        if (keys == null || keys.size() == 0) return;
        for (String key : keys) {
            //get blogId，，这个blogId是在后半段的。
            Long blogId = Long.parseLong(key.substring(prefix.length(), key.length()));//截取，得到blogId

            durableForLikeListByBogId(blogId);//刷新数据到数据库以更新LikeList
            //TODO 在批量上感觉应该还可以优化，已经是updateBatch不过不知道能不能实现，因为这个是根据like_list字段批量更新
        }

    }



    /**    //通过半pipeline技术实现likeList全量持久化数据库
     //TODO 在批量上感觉应该还可以优化，已经是updateBatch不过不知道能不能实现，因为这个是根据字段批量更新
     *
     */
    @Override
    public void updateAllLikeListToDatabaseByPipeline1() {
        String prefix = "BLOG_LIKED_KEY";
//        Set<String> keys = getAllRedisKeyByKeys(prefix);//这个不行！
        Set<String> keys = getAllRedisKeyByScanThroughMatch(prefix);//通过scan拿到以BLOG_LIKED_KEY开头的所有key
        if (keys == null || keys.size() == 0) return;
        Map<Long,Map<String,String>> maps = getIdWithTimeListsByPipelineByJedis(keys);
        if (maps == null || maps.size() == 0) return;

        for (Map.Entry<Long, Map<String, String>> entry : maps.entrySet()) {
            Long blogId = entry.getKey();
            Map<String, String> likeList = entry.getValue();
            updateLikeListByBlogId(blogId,likeList);
        }

    }

    @Autowired
    private JedisPool jedisPool;

    //测试过了，应该没问题
    public Set<String> getAllRedisKeyByScan(String prefix) {//找不到stringRedisTemplate对Zset里键值对扫描的资料
        //这里就直接用其他的Redis接口吧。

        //接下来是scan模糊扫描，获取目标数据
        //RedisTemplate这里搞不定。只能找其他的了

        Jedis jedis = null;
        Set<String> blogLikeList = new HashSet<>();
        try {
            jedis = jedisPool.getResource();
            String cursor = "0";

            do {
                // 扫描并获取一部分key
                ScanResult<String> result = jedis.scan(cursor);
                // 记录cursor
                cursor = result.getCursor();
                List<String> list = result.getResult();
                if (list == null || list.isEmpty()) {
                    break;
                }
                // 遍历
                for (String key : list) {
                    // 判断key的类型
                    String type = jedis.type(key);
                    if (type.equals("zset")) {
                        log.debug("key is {}", key);//这里可以看到有哪些key
                        //符合就要，不符合就不要
                        if (key.substring(0, prefix.length()).equals(prefix)) blogLikeList.add(key);
                    }

                }
            } while (!cursor.equals("0"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) jedis.close();
        }
        return blogLikeList;
    }

    public Set<String> getAllRedisKeyByScanThroughMatch(String prefix) {//找不到stringRedisTemplate对Zset里键值对扫描的资料
        Jedis jedis = null;
        Set<String> blogLikeList = new HashSet<>();
        ScanParams scanParams = new ScanParams();
        try {
            jedis = jedisPool.getResource();
            String cursor = "0";


            do {
                // 扫描并获取一部分key
                ScanResult<String> result = jedis.scan(cursor, scanParams.match(prefix.concat("*")));//https://www.cnblogs.com/xubiao/p/8489522.html
                // 记录cursor
                cursor = result.getCursor();
                List<String> list = result.getResult();
                if (list == null || list.isEmpty()) {
                    break;
                }

                // 遍历
                for (String key : list) {
                    log.debug("key is {}", key);//这里可以看到有哪些key
                    blogLikeList.add(key);
                }

            } while (!cursor.equals("0"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) jedis.close();
        }
        return blogLikeList;
    }


    private Set<String> getAllRedisKeyByKeys(String prefix) {
        Set<String> keys = stringRedisTemplate.keys(prefix.concat("*"));
        return keys;
    }


    public void getByPipelineByJedis(String prefix) {//未完善
        Jedis jedis = jedisPool.getResource();
        Pipeline pipelined = jedis.pipelined();
        pipelined.get(prefix);
        Response<List<Object>> exec = pipelined.exec();
        log.debug("prefix is {},exec is {}", prefix, exec);
        jedis.close();
//        pipelined.
    }



    /**
     * 这个方法没问题,以及测试过了，zrange BLOG_LIKED_KEY1 0 -1 withscores
     * collection表示的是集合：[BLOG_LIKED_KEY+所有博客id]，通过pipeline得到maps
     * maps的键是类似于{1={1=1688218686914, 2=1688398456605}},
     *  后面会写一个api该api可以通过maps更新blog_like表里多行数据的like_list字段（现在是20230704-0：06，以及写好了，就updateAllLikeListToDatabaseByPipeline1这个方法先吧）
     * @param collection 在redis里表示的是存储了的key，在这个项目里，这里是指zset的key的集合
     * @return
     */

    public Map<Long,Map<String,String>> getIdWithTimeListsByPipelineByJedis(Collection<String> collection) {

        String prefix = "BLOG_LIKED_KEY";
        Jedis jedis = null;
        Map<Long,Map<String,String>> maps = null;
        try {
            jedis = jedisPool.getResource();
            Pipeline pipe = jedis.pipelined();
            ArrayList<Long> blogKeyIds = new ArrayList<>(collection.size());
            for (String key : collection) {//TODO 1、控制管道的命令的数量，这里是需要优化的。2、另外还可以多线程发送管道命令，
                pipe.zrangeWithScores(key, 0, -1);
                blogKeyIds.add(Long.parseLong(key.substring(prefix.length(),key.length())));
            }
            List<Object> response = pipe.syncAndReturnAll();//https://blog.csdn.net/ssehs/article/details/108296780
            maps = pipeLineResponseTransformedToMap(response,blogKeyIds);
        } catch (NumberFormatException e) {
            e.printStackTrace();
        } finally {
            jedis.close();
        }
        log.debug("maps is {}",maps);
        return maps;
    }//syncAndReturnAll的参考来源https://blog.csdn.net/ssehs/article/details/108296780

    private Map<Long, Map<String, String>> pipeLineResponseTransformedToMap(List<Object> response,ArrayList<Long> blogKeyIds) {
        if (blogKeyIds == null || blogKeyIds.isEmpty()) return null;
        log.debug("response is {}", response);
        int count = 0;

        if (response == null || response.size() == 0) return null;

        String jsonStr = JSONUtil.toJsonStr(response);
        log.debug("jsonStr is {}",jsonStr);//[[{"score":1.688218686914E12,"element":"1"},{"score":1.688375549984E12,"element":"2"}]]
        List<List> lists = JSONUtil.toList(jsonStr, List.class);

        log.debug("lists is {}",lists);//[[{"score":1688218686914,"element":"1"}, {"score":1688375549984,"element":"2"}]]
        if (lists == null || lists.isEmpty()) return null;


        Map<Long,Map<String,String>> maps = new HashMap<>(blogKeyIds.size());
        for (int i = 0; i < lists.size(); i++) {
            List list = lists.get(i);
            log.debug("list is {}",list);//[{"score":1688218686914,"element":"1"}, {"score":1688375549984,"element":"2"}]

            Long blogKeyId = blogKeyIds.get(i);
            Map<String, String> likeListMap = new HashMap<>();
            maps.put(blogKeyId,likeListMap);
            for (Object o :list) {
                String likedIdTimeStr = JSONUtil.toJsonStr(o);
                LikedIdTime likedIdTime = JSONUtil.toBean(likedIdTimeStr, LikedIdTime.class);
                log.debug("likedIdTime is {}",likedIdTime);
                if (likedIdTime == null) continue;
                likeListMap.put(likedIdTime.getElement(),likedIdTime.getScore());
            }
        }
        return maps;
    }




}
